﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Expression.Encoder.Plugins.Publishing;
using System.Diagnostics;
using System.Net;
using System.IO;
using System.Security.Cryptography;
using System.Globalization;
using System.Xml;
using Sgml;
using System.Text.RegularExpressions;

namespace TimHeuer.Expression.FtpPublish
{
    [EncoderPlugin("FTP Server", "Publish to your own FTP server location")]
    public class Publish : PublishPlugin
    {
        #region Constants
        const string DEFAULT_TEMPLATE_FILE = "default.html";
        const string FILE_REGEX = @"(?<FileName>[a-zA-z0-9]:((\\([-*\.*\w+\s+\d+]+)|(\w+)\\)+)(\w+.PNG)|(\w+.png)|([a-zA-z0-9_.]+.(jpg|JPEG|png|PNG|WMV|wmv|WMA|wma|MP4|mp4|xap|XAP)))";
        #endregion

        #region Member Variables
        private PublishData _data;
        private StandardSettings _standard;
        private AdvancedSettings _advanced;
        private bool _cancelled = false;
        private string _currentFile = string.Empty;
        #endregion

        #region Constructors
        public Publish():this(new PublishData())
        {
        }

        public Publish(PublishData data)
        {
            _data = data;
            _standard = new StandardSettings(_data, this);
            _advanced = new AdvancedSettings(_data, this);
        }
        #endregion

        #region Overrides
        public override void LoadJobSettings(System.Xml.XmlReader reader)
        {
            reader.ReadStartElement("FtpJob");
            try
            {
                this._data.ServerAddress = getValueAsString(reader, "ServerAddress");
                this._data.UserName = getValueAsString(reader, "UserName");
                this._data.Password = DecodeKey(getValueAsString(reader, "Password"));
                this._data.DefaultDirectory = getValueAsString(reader, "DefaultDirectory");
                this._data.UseProxy = getValueAsBool(reader, "UseProxy");
                this._data.BaseUri = getValueAsString(reader, "BaseUri");
                this._data.SavePassword = getValueAsBool(reader, "SavePassword");
                //this._data.GenerateHtml = getValueAsBool(reader, "GenerateHtml");
            }
            catch (Exception ex)
            {
            }
            reader.ReadEndElement();
        }

        public override void LoadUISettings(System.Xml.XmlReader reader)
        {
            reader.ReadStartElement("FtpJobUI");
            try
            {
                this._data.ServerAddress = getValueAsString(reader, "ServerAddress");
                this._data.UserName = getValueAsString(reader, "UserName");
                this._data.Password = DecodeKey(getValueAsString(reader, "Password"));
                this._data.DefaultDirectory = getValueAsString(reader, "DefaultDirectory");
                this._data.UseProxy = getValueAsBool(reader, "UseProxy");
                this._data.BaseUri = getValueAsString(reader, "BaseUri");
                this._data.SavePassword = getValueAsBool(reader, "SavePassword");
                //this._data.GenerateHtml = getValueAsBool(reader, "GenerateHtml");
            }
            catch (Exception ex)
            {
            }
            reader.ReadEndElement();
        }

        public override void SaveJobSettings(System.Xml.XmlWriter writer)
        {
            writer.WriteStartElement("FtpJob");
            writer.WriteElementString("ServerAddress", _data.ServerAddress);
            writer.WriteElementString("UserName", _data.UserName);
            writer.WriteElementString("Password", (_data.SavePassword) ? EncodeKey(_data.Password) : string.Empty);
            writer.WriteElementString("DefaultDirectory", _data.DefaultDirectory);
            writer.WriteElementString("UseProxy", _data.UseProxy.ToString());
            writer.WriteElementString("BaseUri", _data.BaseUri);
            writer.WriteElementString("SavePassword", _data.SavePassword.ToString());
            //writer.WriteElementString("GenerateHtml", _data.GenerateHtml.ToString());
            writer.WriteEndElement();
        }

        public override void SaveUISettings(System.Xml.XmlWriter writer)
        {
            writer.WriteStartElement("FtpJobUI");
            writer.WriteElementString("ServerAddress", _data.ServerAddress);
            writer.WriteElementString("UserName", _data.UserName);
            writer.WriteElementString("Password", (_data.SavePassword) ? EncodeKey(_data.Password) : string.Empty);
            writer.WriteElementString("DefaultDirectory", _data.DefaultDirectory);
            writer.WriteElementString("UseProxy", _data.UseProxy.ToString());
            writer.WriteElementString("BaseUri", _data.BaseUri);
            writer.WriteElementString("SavePassword", _data.SavePassword.ToString());
            //writer.WriteElementString("GenerateHtml", _data.GenerateHtml.ToString());
            writer.WriteEndElement();
        }

        protected override void CancelPublish()
        {
            _cancelled = true;
            base.CancelPublish();
        }
        
        public override object CreateAdvancedSettingsEditor()
        {
            if (_advanced == null)
            {
                try
                {
                    this._advanced = new AdvancedSettings(_data, this);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                    throw;
                }
            }
            return _advanced;
        }

        public override object CreateStandardSettingsEditor()
        {
            if (_standard == null)
            {
                try
                {
                    this._standard = new StandardSettings(_data, this);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                    throw;
                }
            }
            return _standard;
        }

        public override void PerformPublish(string rootPath, string[] filesToPublish)
        {
            int fileCount = filesToPublish.Length;
            int progressSize = 100 / fileCount;
            int currentProgress = 0;

            string htmlFilePath = string.Empty;
            string mediaFilePath = string.Empty;
            string xapFilePath = string.Empty;

            try
            {
                WebClient ftp = new WebClient();
                if (!_data.UseProxy)
                    ftp.Proxy = null;
                ftp.BaseAddress = string.Format("ftp://{0}/", _data.ServerAddress);

                string fileName = string.Empty;

                ftp.Credentials = new NetworkCredential(_data.UserName, _data.Password);

                foreach (var file in filesToPublish)
                {
                    if (!_cancelled)
                    {
                        fileName = Path.GetFileName(file);
                        
                        if (fileName.ToLower() == DEFAULT_TEMPLATE_FILE)
                        {
                            htmlFilePath = file;
                        }
                        else if (fileName.ToLower().Contains(".xap"))
                        {
                            xapFilePath = Path.GetFileName(file);
                        }
                        else if (fileName.ToLower().EndsWith(".wmv") || fileName.ToLower().EndsWith(".wma") || fileName.ToLower().EndsWith(".mp4"))
                        {
                            mediaFilePath = Path.GetFileName(file);
                        }

                        _currentFile = fileName;
                        if (!string.IsNullOrEmpty(_data.DefaultDirectory))
                        {
                            fileName = string.Concat(_data.DefaultDirectory, "\\", fileName);
                        }
                        currentProgress += progressSize;
                        OnProgress(string.Format("Uploading {0}", _currentFile), currentProgress);
                        ftp.UploadFile(fileName, file);
                    }
                }

                if (_cancelled)
                {
                    PublishingHost.ShowMessageBox("Upload cancelled", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
                }

                #region Templating Code
                // verify a template is being used
                if (this.PublishingHost.CurrentJob.Template != null)
                {
                    // verify the template is a Silverlight 2 template
                    if (this.PublishingHost.CurrentJob.Template.Capabilities.ToString().ToLower().Contains(Microsoft.Expression.Encoder.Templates.TemplateCapabilities.Silverlight20.ToString().ToLower()))
                    {
                        // make sure we have the default.html file
                        if (!string.IsNullOrEmpty(htmlFilePath))
                        {
                            // retrieve the object tag data from the generated template
                            string objectTagData = RetrieveObjectTagContents(htmlFilePath);

                            // if the data is good, then continue
                            if (!string.IsNullOrEmpty(objectTagData))
                            {
                                if (!string.IsNullOrEmpty(this._data.BaseUri))
                                {
                                    objectTagData = ReplaceObjectParams(mediaFilePath, xapFilePath, objectTagData);
                                }                                
                            }

                            // display the objectTagData in the UI
                            this._data.GeneratedHtml = objectTagData;
                        }
                    }
                }
                #endregion
            }
            catch (Exception ex)
            {
                PublishingHost.ShowMessageBox(ex.Message, System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
            }
        }

        #endregion

        #region Helpers
        private string ReplaceObjectParams(string mediaFilePath, string xapFilePath, string objectTagData)
        {
            // replace image/media/xap references
            objectTagData = Regex.Replace(objectTagData, FILE_REGEX, delegate(Match m)
            {
                string v = m.ToString();
                return AppendBaseUri(v);
            }, RegexOptions.ExplicitCapture);

            return objectTagData;
        }

        private string AppendBaseUri(string filePath)
        {
            string trailingSlash = "/";

            if (this._data.BaseUri.EndsWith("/"))
            {
                trailingSlash = string.Empty;
            }
            return string.Concat(this._data.BaseUri, trailingSlash, filePath);
        }

        private static string RetrieveObjectTagContents(string fileName)
        {
            StreamReader rdr = new StreamReader(fileName);
            string html = rdr.ReadToEnd();
            string finalxml = string.Empty;

            Sgml.SgmlReader sgml = new Sgml.SgmlReader();
            sgml.DocType = "html";
            sgml.CaseFolding = Sgml.CaseFolding.ToLower;
            sgml.InputStream = new StringReader(html);

            StringWriter writer = new StringWriter();
            XmlWriter xml = new XmlTextWriter(writer);
            bool insideObject = false;

            while (sgml.Read() && !sgml.EOF)
            {
                switch (sgml.NodeType)
                {
                    case System.Xml.XmlNodeType.Attribute:
                        break;
                    case System.Xml.XmlNodeType.CDATA:
                        break;
                    case System.Xml.XmlNodeType.Comment:
                        break;
                    case System.Xml.XmlNodeType.Document:
                        break;
                    case System.Xml.XmlNodeType.DocumentFragment:
                        break;
                    case System.Xml.XmlNodeType.DocumentType:
                        break;
                    case System.Xml.XmlNodeType.Element:
                        if (sgml.LocalName == "object")
                        {
                            xml.WriteStartElement(sgml.LocalName);
                            xml.WriteAttributes(sgml, true);
                            insideObject = true;
                        }
                        else
                        {
                            if (insideObject)
                            {
                                if (sgml.IsEmptyElement)
                                {
                                    xml.WriteStartElement(sgml.LocalName);
                                    xml.WriteAttributes(sgml, true);
                                    xml.WriteEndElement();
                                }
                                else
                                {
                                    xml.WriteStartElement(sgml.LocalName);
                                    xml.WriteAttributes(sgml, true);
                                }
                            }
                        }
                        break;
                    case System.Xml.XmlNodeType.EndElement:
                        if (sgml.LocalName == "object")
                        {
                            xml.WriteFullEndElement();
                            insideObject = false;
                        }
                        else
                        {
                            if (insideObject)
                            {
                                if (sgml.LocalName == "a")
                                {
                                    xml.WriteFullEndElement();
                                }
                                else
                                {
                                    xml.WriteEndElement();
                                }
                            }
                        }
                        break;
                    case System.Xml.XmlNodeType.EndEntity:
                        break;
                    case System.Xml.XmlNodeType.Entity:
                        break;
                    case System.Xml.XmlNodeType.EntityReference:
                        break;
                    case System.Xml.XmlNodeType.None:
                        break;
                    case System.Xml.XmlNodeType.Notation:
                        break;
                    case System.Xml.XmlNodeType.ProcessingInstruction:
                        break;
                    case System.Xml.XmlNodeType.SignificantWhitespace:
                        break;
                    case System.Xml.XmlNodeType.Text:
                        break;
                    case System.Xml.XmlNodeType.Whitespace:
                        break;
                    case System.Xml.XmlNodeType.XmlDeclaration:
                        break;
                    default:
                        break;
                }
            }

            xml.Close();

            finalxml = writer.ToString();

            return finalxml;
        }

        private static string EncodeKey(string strKey)
        {
            byte[] buffer = ProtectedData.Protect(Encoding.Unicode.GetBytes(strKey), null, DataProtectionScope.CurrentUser);
            StringBuilder builder = new StringBuilder(2 * buffer.Length);
            for (int i = 0; i < buffer.Length; i++)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", new object[] { buffer[i] });
            }
            return builder.ToString();
        }

        private static string DecodeKey(string strEncoding)
        {
            if (string.IsNullOrEmpty(strEncoding))
            {
                return strEncoding;
            }
            if ((strEncoding.Length % 2) != 0)
            {
                return string.Empty;
            }
            byte[] encryptedData = new byte[strEncoding.Length / 2];
            for (int i = 0; i < encryptedData.Length; i++)
            {
                string s = strEncoding.Substring(i * 2, 2);
                byte.Parse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                if (!byte.TryParse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out encryptedData[i]))
                {
                    return string.Empty;
                }
            }
            try
            {
                byte[] bytes = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);
                return Encoding.Unicode.GetString(bytes);
            }
            catch (CryptographicException exception)
            {
                Debug.WriteLine(exception.ToString());
                return string.Empty;
            }
        }
        
        private static string getValueAsString(XmlReader xr, string ElementName)
        {
            try
            {
                return xr.ReadElementString(ElementName);
            }
            catch (XmlException ex)
            {
                return "";
            }
        }
        
        private static bool getValueAsBool(XmlReader xr, string ElementName)
        {
            try
            {
                return bool.Parse(xr.ReadElementString(ElementName));
            }
            catch (XmlException ex)
            {
                return true;
            }
        }
        #endregion
    }
}